implement YAML;

#
# YAML - Ain't Markup Language
#
# simple implementation of YAML
# Copyright © 2014 leonid.kovalenko@tereveni.org 
#
# ToDo: 
# more compatible with "http://www.yaml.org/spec/1.2/spec.html"


include "sys.m";
	sys: Sys;

include "bufio.m";
	bufio: Bufio;
	Iobuf: import bufio;

include "yaml.m";


#################### GLOBAL #####################

Syntax: exception(string);


################### EXPORTED ####################

init() {

	sys = load Sys Sys->PATH;
	bufio = load Bufio Bufio->PATH;
}

# get value from map by key
YValue.get(y: self ref YValue, k: string): ref YValue {

	pick r := y {
	
		Map =>
			for(l := r.data; l != nil; l = tl l)
				
				if((hd l).t0 == k)
					return (hd l).t1;
			
			return nil;
		* =>
			return nil;
	}
}

# parse string to YAML
YValue.parse(s: string): (ref YValue, string) {
	
	fd := bufio->sopen(s);
	{
		
		# start parser
		p := Parse.new(fd, 0);
		y := p.parseitem();
		return (y, "");

	} exception e {
	
		Syntax =>
			return (nil, sys->sprint("YAML syntax error (offset %bd): %s", fd.offset(), e));
	}
}

# convert YAML to string
YValue.text(y: self ref YValue): string {
	
	s := "";
	(a, nil) := value2strings(y);
	for(i := 0; i < len a; i++){

		if (i != 0)
			s += "\n";
		s += a[i];
	}
	return s;
}


##################### OTHER #####################

# parser
Parse: adt {
	
	input:		ref Iobuf;
	level:		int;

	new:		fn(io: ref Iobuf, level: int): ref Parse;
	parseitem:	fn(p: self ref Parse): ref YValue raises (Syntax);
	readstring:	fn(p: self ref Parse, d: array of int) : string;
	unget:		fn(p: self ref Parse, o: big);

};

# new parser
Parse.new(input: ref Iobuf, level: int): ref Parse {
	
	return ref Parse(input, level);
}

# parse text - return YAML
Parse.parseitem(p: self ref Parse) : ref YValue raises (Syntax) {
	
	y,yp : ref YValue;

	{	

		do {

			# offset of value
			o := p.input.offset();

			# check file
			if ((c := p.input.getc()) == Bufio->EOF)
				return y;
			else	
				p.input.ungetc();

			# calc level of atom
			lvl := -1;
			while((c = p.input.getc()) == ' ' || c == '\n') {
		
				if (lvl > -1 && c == ' ')
					lvl++;
				else if (c == '\n')
					lvl = 0;
			}
			p.input.ungetc();
			
			# check level of atom
			if (lvl > -1 && p.level > lvl) {
				
				p.unget(o);
				return y;					
			}	

			# read string
			d := array[] of {' ', '\t', '\n', '\r'};
			s := p.readstring(d);

			# create value
			if (s[(len s) - 1] == ':') {

				if (y == nil)
					y = ref YValue.Map(nil);

				# check value
				pick yy := y {

					Map =>
				
						# child value
						pp := Parse.new(p.input, p.level + 1);
						yp = pp.parseitem();			

						# add child value
						if (yp != nil) {

							# keyword of map
							kw : string;
							if ((len s) > 1) {
								kw = s[0:(len s) - 1];
							} else {
								kw = "";
							};

							# add to list
							rl:  list of (string, ref YValue);							
							for(l := yy.data; l != nil; l = tl l)
								rl = hd l :: rl;
							rl = (kw, yp) :: rl;
							for(; rl != nil; rl = tl rl)
								l = hd rl :: l;
							yy.data = l;									
						}					
					* =>

						raise Syntax("expected <block end>");
				}

			} else if (s == "-") {

				if (y == nil)
					y = ref YValue.Seq(nil);	

				pick yy := y {

					Seq =>
					
						# child value
						pp := Parse.new(p.input, p.level + 1);
						yp = pp.parseitem();

						# add child value
						if (yp != nil) {
							
							na := array[len yy.data + 1] of ref YValue;
							if (len yy.data)	
								na[0:] = yy.data[0:];						  
							na[len yy.data] = yp;				
							yy.data = na;
							y = yy;
						}
					* =>

						raise Syntax("expected <block end>");
				}

			} else {

				# read string to end
				if((c = p.input.getc()) > -1) {				
					p.unget(o);
					d = array[] of {'\n', '\r'};
					s = p.readstring(d);
				}

				case s {
					
					"true" =>	
							
						y = ref YValue.True();
					"false" =>	
							
						y = ref YValue.False();
					"null" =>	
							
						y = ref YValue.Null();
					* =>	
							
						y = ref YValue.Str(s);
				}
			}	
		} while (yp != nil);

		return y;
	} exception {
		
		Syntax => raise;
	}	
}

# unget to offset
Parse.unget(p: self ref Parse, o: big) {

	while ((d := p.input.offset()) > o) {

		p.input.ungetc();
	}
}

# read string
Parse.readstring(p: self ref Parse, d: array of int) : string {

	s : string;
	while((c := p.input.getc()) >= 0) { 

		for (i := 0; i < len d; i++) {

			if (c != d[i]) 
				continue;
			
			p.input.ungetc();
			return s;			
		}

		s[len s] = c;
	}

	p.input.ungetc();
	return s;		
}


# convert value to array of strings
value2strings(y: ref YValue) : (array of string, int) {

	ra := array[0] of string;
	nl := 0;	# flag of "new line"

	if(y == nil)
		return (ra, nl);		

	pick r := y {
		
		Null =>

			ra = array[] of {"null"};				
		True =>

			ra = array[] of {"true"};
		False =>	

			ra = array[] of {"false"};
		Big =>
			
			ra = array[] of {string r.value};									
		Str =>
		
			ra = array[] of {r.value};

		Seq =>
				
			# fill result
			for(i := 0; i < len(r.data); i++){
								
				ra = addstrings(ra, "-", r.data[i]);
			};	

			nl = len(r.data) > 1; 		
		Map =>						
			
			# fill result 
			for(l := r.data; l != nil; l = tl l) { 
						
				(key, val) := hd l;				
				ra = addstrings(ra, key+":", val);
			};
			nl = len(r.data) > 1;
		* =>
		
			ra = array[] of {""};
	}

	# return strings and flag of "new line"
	return (ra, nl);
}

# pa - previous array of strings
# prfx - text prefix of value
# val - value
addstrings(pa: array of string, prfx: string, val: ref YValue) : array of string {
	
	# array of string of value
	(va, line) := value2strings(val);
	
	# len of arrays
	pn := len(pa);	
	vn := len(va);

	# new array
	a : array of string;
	if (line) 
		a = array[pn + vn + 1] of (string);
	else 
		a = array[pn + vn] of (string);

	# fill new array
	for(i := pn; i < len a; i++){

		if (line && i == pn) {

			a[i] = prfx; 						# create first string with "new line"
		} else if (line) {

			a[i] = " " + va[i - pn - 1];		# create data string with "new line"
		} else if (i == pn) {

			a[i] = prfx + " " + va[i - pn];		# create first data string without "new line"
		} else {

			a[i] = " " + va[i - pn];			# create data string without "new line"			
		}	
	};
	if(pn)
		a[0:] = pa[0:];
	
	# return new array
	return a;		
}
